--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Commit 050b3aa17afdfeb4a2fd58c5bc411a9f1de7e0e0
Parents : 912f86e
Author : Mark Qvist <mark@unsigned.io>
Date : 2024-06-04T04:46:47+02:00
Improved audio playback on Linux
Changes
3 files changed, 70 insertions(+), 32 deletions(-)
Diff
diff --git a/sbapp/main.py b/sbapp/main.py
index eb803f25..feb5cfa4 100644
--- a/sbapp/main.py
+++ b/sbapp/main.py
@@ -1534,8 +1534,11 @@ class SidebandApp(MDApp):
elif audio_field[0] >= LXMF.AM_CODEC2_700C and audio_field[0] <= LXMF.AM_CODEC2_3200:
temp_path = self.sideband.rec_cache+"/msg.ogg"
from sideband.audioproc import samples_to_ogg, decode_codec2
- if samples_to_ogg(decode_codec2(audio_field[1], audio_field[0]), temp_path):
- RNS.log("Wrote wav file to: "+temp_path)
+ samples = decode_codec2(audio_field[1], audio_field[0])
+ if samples_to_ogg(samples, temp_path):
+ RNS.log("Wrote OGG file to: "+temp_path, RNS.LOG_DEBUG)
+ else:
+ RNS.log("OGG write failed", RNS.LOG_DEBUG)
else:
raise NotImplementedError(audio_field[0])
diff --git a/sbapp/plyer/platforms/linux/audio.py b/sbapp/plyer/platforms/linux/audio.py
index 18ba9f21..3509f336 100644
--- a/sbapp/plyer/platforms/linux/audio.py
+++ b/sbapp/plyer/platforms/linux/audio.py
@@ -1,7 +1,8 @@
import time
import threading
+import RNS
from sbapp.plyer.facades.audio import Audio
-from kivy.core.audio import SoundLoader
+from ffpyplayer.player import MediaPlayer
class LinuxAudio(Audio):
@@ -18,8 +19,13 @@ class LinuxAudio(Audio):
self.is_playing = False
def _check_playback(self):
- while self.sound != None and self.sound.state == "play":
+ run = True
+ while run and self.sound != None and not self.sound.get_pause():
time.sleep(0.25)
+ if self.duration:
+ pts = self.sound.get_pts()
+ if pts > self.duration:
+ run = False
self.is_playing = False
@@ -32,23 +38,30 @@ class LinuxAudio(Audio):
pass
def _stop(self):
- if self.sound != None and self.sound.state == "play":
- self.sound.stop()
+ if self.sound != None:
+ self.sound.set_pause(True)
+ self.sound.seek(0, relative=False)
self.is_playing = False
def _play(self):
- if self.sound == None or self._loaded_path != self._file_path:
- self.sound = SoundLoader.load(self._file_path)
+ self.sound = MediaPlayer(self._file_path)
+ self.metadata = self.sound.get_metadata()
+ self.duration = self.metadata["duration"]
+ if self.duration == None:
+ time.sleep(0.15)
+ self.metadata = self.sound.get_metadata()
+ self.duration = self.metadata["duration"]
+ RNS.log(str(self.metadata))
+
+ self._loaded_path = self._file_path
self.is_playing = True
- self.sound.play()
self._check_thread = threading.Thread(target=self._check_playback, daemon=True)
self._check_thread.start()
def reload(self):
self._loaded_path = None
- self.sound = None
def playing(self):
return self.is_playing
diff --git a/sbapp/sideband/audioproc.py b/sbapp/sideband/audioproc.py
index 740bf303..1993057c 100644
--- a/sbapp/sideband/audioproc.py
+++ b/sbapp/sideband/audioproc.py
@@ -43,28 +43,48 @@ def samples_from_ogg(file_path=None):
return audio.get_array_of_samples()
def samples_to_ogg(samples=None, file_path=None):
- if file_path != None and samples != None:
- pcm_data = io.BytesIO(samples)
- channels = 1; samples_per_second = 8000; bytes_per_sample = 2
-
- opus_buffered_encoder = pyogg.OpusBufferedEncoder()
- opus_buffered_encoder.set_application("audio")
- opus_buffered_encoder.set_sampling_frequency(samples_per_second)
- opus_buffered_encoder.set_channels(channels)
- opus_buffered_encoder.set_frame_size(20) # milliseconds
- ogg_opus_writer = pyogg.OggOpusWriter(file_path, opus_buffered_encoder)
-
- frame_duration = 0.1
- frame_size = int(frame_duration * samples_per_second)
- bytes_per_frame = frame_size*bytes_per_sample
-
- while True:
- pcm = pcm_data.read(bytes_per_frame)
- if len(pcm) == 0:
- break
- ogg_opus_writer.write(memoryview(bytearray(pcm)))
-
- ogg_opus_writer.close()
+ try:
+ if file_path != None and samples != None:
+ pcm_data = io.BytesIO(samples)
+
+ RNS.log(f"Samples: {len(samples)}")
+ RNS.log(f"Type : {type(samples)}")
+
+ channels = 1; samples_per_second = 8000; bytes_per_sample = 2
+
+ opus_buffered_encoder = pyogg.OpusBufferedEncoder()
+ opus_buffered_encoder.set_application("audio")
+ opus_buffered_encoder.set_sampling_frequency(samples_per_second)
+ opus_buffered_encoder.set_channels(channels)
+ opus_buffered_encoder.set_frame_size(20) # milliseconds
+ ogg_opus_writer = pyogg.OggOpusWriter(file_path, opus_buffered_encoder)
+
+ frame_duration = 0.020
+ frame_size = int(frame_duration * samples_per_second)
+ bytes_per_frame = frame_size*bytes_per_sample
+
+ read_bytes = 0
+ written_bytes = 0
+ while True:
+ pcm = pcm_data.read(bytes_per_frame)
+ if len(pcm) == 0:
+ break
+ else:
+ read_bytes += len(pcm)
+
+ ogg_opus_writer.write(memoryview(bytearray(pcm)))
+ written_bytes += len(pcm)
+
+ ogg_opus_writer.close()
+
+ RNS.log(f"Read {read_bytes} bytes")
+ RNS.log(f"Wrote {written_bytes} bytes")
+
+ return True
+
+ except Exception as e:
+ RNS.trace_exception(e)
+ return False
def samples_to_wav(samples=None, file_path=None):
if samples != None and file_path != None:
@@ -87,7 +107,9 @@ def encode_codec2(samples, mode):
SPF = c2.samples_per_frame()
PACKET_SIZE = SPF * 2 # 16-bit samples
STRUCT_FORMAT = '{}h'.format(SPF)
+ F_FRAMES = len(samples)/SPF
N_FRAMES = math.floor(len(samples)/SPF)
+ # TODO: Add padding to align to whole frames
frames = np.array(samples[0:N_FRAMES*SPF], dtype=np.int16)
encoded = b""
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────